/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.response.transform;

import static org.hamcrest.core.StringContains.containsString;

import java.util.Collection;
import java.util.Iterator;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.BasicResultContext;
import org.apache.solr.util.RandomNoReverseMergePolicyFactory;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TestRule;

public class TestChildDocTransformer extends SolrTestCaseJ4 {

  private static String ID_FIELD = "id";
  private String[] titleVals = new String[2];

  @ClassRule
  public static final TestRule noReverseMerge = RandomNoReverseMergePolicyFactory.createRule();

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig.xml", "schema-root.xml"); // *not* the "nest" schema
  }

  @After
  public void cleanup() {
    assertU(delQ("*:*"));
    assertU(commit());
  }

  @Test
  public void testParentFilter() throws Exception {
    for (int i = 0; i < titleVals.length; i++) {
      titleVals[i] = TestUtil.randomSimpleString(random(), 1, 20);
    }
    createIndex(titleVals);
    testParentFilterJSON();
    testParentFilterXML();
    testSubQueryParentFilterJSON();
    testSubQueryParentFilterXML();
  }

  @Test
  public void testAllParams() throws Exception {
    createSimpleIndex();
    testChildDoctransformerJSON();
    testChildDoctransformerXML();

    testSubQueryXML();
    testSubQueryJSON();

    testChildDocNonStoredDVFields();
    testChildReturnFields();
  }

  private void testChildDoctransformerXML() {
    String test1[] =
        new String[] {
          "//*[@numFound='1']",
          "/response/result/doc[1]/doc[1]/str[@name='id']='2'",
          "/response/result/doc[1]/doc[2]/str[@name='id']='3'",
          "/response/result/doc[1]/doc[3]/str[@name='id']='4'",
          "/response/result/doc[1]/doc[4]/str[@name='id']='5'",
          "/response/result/doc[1]/doc[5]/str[@name='id']='6'",
          "/response/result/doc[1]/doc[6]/str[@name='id']='7'"
        };

    String test2[] =
        new String[] {
          "//*[@numFound='1']",
          "/response/result/doc[1]/doc[1]/str[@name='id']='2'",
          "/response/result/doc[1]/doc[2]/str[@name='id']='4'",
          "/response/result/doc[1]/doc[3]/str[@name='id']='6'"
        };

    String test3[] =
        new String[] {
          "//*[@numFound='1']",
          "count(/response/result/doc[1]/doc)=2",
          "/response/result/doc[1]/doc[1]/str[@name='id']='3'",
          "/response/result/doc[1]/doc[2]/str[@name='id']='5'"
        };

    assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ", "fl", "*,[child]"), test1);

    // shows parentFilter specified (not necessary anymore) and also child
    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id, subject,[child childFilter=\"title:foo\"]"),
        test2);

    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id, subject,[child childFilter=\"title:bar\" limit=2]"),
        test3);

    SolrException e =
        expectThrows(
            SolrException.class,
            () -> {
              h.query(
                  req(
                      "q",
                      "*:*",
                      "fq",
                      "subject:\"parentDocument\" ",
                      "fl",
                      "id, subject,[child parentFilter=\"subject:bleh\" childFilter=\"title:bar\" limit=2]"));
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
    assertThat(
        e.getMessage(),
        containsString(
            "Parent filter 'QueryBitSetProducer(subject:bleh)' doesn't match any parent documents"));

    e =
        expectThrows(
            SolrException.class,
            () -> {
              h.query(
                  req(
                      "q",
                      "*:*",
                      "fq",
                      "subject:\"parentDocument\" ",
                      "fl",
                      "id, subject,[child parentFilter=e childFilter=\"title:bar\" limit=2]"));
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
    assertThat(
        e.getMessage(),
        containsString(
            "Parent filter 'QueryBitSetProducer(text:e)' doesn't match any parent documents"));

    e =
        expectThrows(
            SolrException.class,
            () -> {
              h.query(
                  req(
                      "q",
                      "*:*",
                      "fq",
                      "subject:\"parentDocument\" ",
                      "fl",
                      "id, subject,[child parentFilter=\"\"]"));
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
    assertThat(e.getMessage(), containsString("Invalid Parent filter '', resolves to null"));
  }

  private void testSubQueryXML() {
    String test1[];
    {
      final String subqueryPath = "/result[@name='children'][@numFound='6']";
      test1 =
          new String[] {
            "//*[@numFound='1']",
            "/response/result/doc[1]" + subqueryPath + "/doc[1]/str[@name='id']='2'",
            "/response/result/doc[1]" + subqueryPath + "/doc[2]/str[@name='id']='3'",
            "/response/result/doc[1]" + subqueryPath + "/doc[3]/str[@name='id']='4'",
            "/response/result/doc[1]" + subqueryPath + "/doc[4]/str[@name='id']='5'",
            "/response/result/doc[1]" + subqueryPath + "/doc[5]/str[@name='id']='6'",
            "/response/result/doc[1]" + subqueryPath + "/doc[6]/str[@name='id']='7'"
          };
    }

    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.rows",
            "10"),
        test1);

    String test2[] =
        new String[] {
          "//*[@numFound='1']",
          "/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[1]/str[@name='id']='2'",
          "/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[2]/str[@name='id']='4'",
          "/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[3]/str[@name='id']='6'"
        };

    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.rows",
            "10",
            "children.fq",
            "title:foo"),
        test2);

    String test3[] =
        new String[] {
          "//*[@numFound='1']",
          "/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[1]/str[@name='id']='3'",
          "/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[2]/str[@name='id']='5'"
        };

    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.rows",
            "2",
            "children.fq",
            "title:bar",
            "children.sort",
            "_docid_ asc"),
        test3);
  }

  private void testSubQueryJSON() throws Exception {
    String[] test1 =
        new String[] {
          "/response/docs/[0]/children/docs/[0]/id=='2'",
          "/response/docs/[0]/children/docs/[1]/id=='3'",
          "/response/docs/[0]/children/docs/[2]/id=='4'",
          "/response/docs/[0]/children/docs/[3]/id=='5'",
          "/response/docs/[0]/children/docs/[4]/id=='6'",
          "/response/docs/[0]/children/docs/[5]/id=='7'"
        };

    String[] test2 =
        new String[] {
          "/response/docs/[0]/children/docs/[0]/id=='2'",
          "/response/docs/[0]/children/docs/[1]/id=='4'",
          "/response/docs/[0]/children/docs/[2]/id=='6'"
        };

    String[] test3 =
        new String[] {
          "/response/docs/[0]/children/docs/[0]/id=='3'",
          "/response/docs/[0]/children/docs/[1]/id=='5'"
        };

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.rows",
            "10"),
        test1);

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.rows",
            "10",
            "children.fq",
            "title:foo"),
        test2);

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.rows",
            "2",
            "children.fq",
            "title:bar",
            "children.sort",
            "_docid_ asc"),
        test3);
  }

  private void testChildDoctransformerJSON() throws Exception {
    String[] test1 =
        new String[] {
          "/response/docs/[0]/_childDocuments_/[0]/id=='2'",
          "/response/docs/[0]/_childDocuments_/[1]/id=='3'",
          "/response/docs/[0]/_childDocuments_/[2]/id=='4'",
          "/response/docs/[0]/_childDocuments_/[3]/id=='5'",
          "/response/docs/[0]/_childDocuments_/[4]/id=='6'",
          "/response/docs/[0]/_childDocuments_/[5]/id=='7'"
        };

    String[] test2 =
        new String[] {
          "/response/docs/[0]/_childDocuments_/[0]/id=='2'",
          "/response/docs/[0]/_childDocuments_/[1]/id=='4'",
          "/response/docs/[0]/_childDocuments_/[2]/id=='6'"
        };

    String[] test3 =
        new String[] {
          "/response/docs/[0]/_childDocuments_/[0]/id=='3'",
          "/response/docs/[0]/_childDocuments_/[1]/id=='5'"
        };

    assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ", "fl", "*,[child]"), test1);

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id, subject,[child childFilter=\"title:foo\"]"),
        test2);

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id, subject,[child childFilter=\"title:bar\" limit=3]"),
        test3);
  }

  private void testChildDocNonStoredDVFields() throws Exception {
    String[] test1 =
        new String[] {
          "/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[1]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[2]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[3]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[4]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[5]/intDvoDefault==42"
        };

    String[] test2 =
        new String[] {
          "/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[1]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[2]/intDvoDefault==42"
        };

    String[] test3 =
        new String[] {
          "/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
          "/response/docs/[0]/_childDocuments_/[1]/intDvoDefault==42"
        };

    assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ", "fl", "*,[child]"), test1);

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "intDvoDefault, subject,[child childFilter=\"title:foo\"]"),
        test2);

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "intDvoDefault, subject,[child childFilter=\"title:bar\" limit=2]"),
        test3);
  }

  private void testChildReturnFields() throws Exception {

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,[child fl=\"intDvoDefault,child_fl:[value v='child_fl_test']\"]"),
        "/response/docs/[0]/intDefault==42",
        "/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
        "/response/docs/[0]/_childDocuments_/[0]/child_fl=='child_fl_test'");

    try (SolrQueryRequest req =
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "intDefault,[child fl=\"intDvoDefault, [docid]\"]")) {
      BasicResultContext res =
          (BasicResultContext) h.queryAndResponse("/select", req).getResponse();
      Iterator<SolrDocument> docsStreamer = res.getProcessedDocuments();
      while (docsStreamer.hasNext()) {
        SolrDocument doc = docsStreamer.next();
        assertFalse(
            "root docs should not contain fields specified in child return fields",
            doc.containsKey("intDvoDefault"));
        assertTrue(
            "root docs should contain fields specified in query return fields",
            doc.containsKey("intDefault"));
        Collection<SolrDocument> childDocs = doc.getChildDocuments();
        for (SolrDocument childDoc : childDocs) {
          assertEquals("child doc should only have 2 keys", 2, childDoc.keySet().size());
          assertTrue(
              "child docs should contain fields specified in child return fields",
              childDoc.containsKey("intDvoDefault"));
          assertEquals(
              "child docs should contain fields specified in child return fields",
              42,
              childDoc.getFieldValue("intDvoDefault"));
          assertTrue(
              "child docs should contain fields specified in child return fields",
              childDoc.containsKey("[docid]"));
        }
      }
    }
  }

  private void createSimpleIndex() {

    SolrInputDocument parentDocument = new SolrInputDocument();
    parentDocument.addField(ID_FIELD, "1");
    parentDocument.addField("subject", "parentDocument");
    for (int i = 0; i < 6; i++) {
      SolrInputDocument childDocument = new SolrInputDocument();
      childDocument.addField(ID_FIELD, Integer.toString(i + 2));
      if (i % 2 == 0) {
        childDocument.addField("title", "foo");
      } else {
        childDocument.addField("title", "bar");
      }

      parentDocument.addChildDocument(childDocument);
    }
    try {
      Long version = addAndGetVersion(parentDocument, null);
      assertNotNull(version);
    } catch (Exception e) {
      fail("Failed to add document to the index");
    }
    assertU(commit());
    assertQ(req("q", "*:*"), "//*[@numFound='" + 7 + "']");
  }

  private static void createIndex(String[] titleVals) {

    String[] parentIDS = new String[] {"1", "4"};
    String[] childDocIDS = new String[] {"2", "5"};
    String[] grandChildIDS = new String[] {"3", "6"};

    for (int i = 0; i < parentIDS.length; i++) {
      SolrInputDocument parentDocument = new SolrInputDocument();
      parentDocument.addField(ID_FIELD, parentIDS[i]);
      parentDocument.addField("subject", "parentDocument");
      parentDocument.addField("title", titleVals[i]);

      SolrInputDocument childDocument = new SolrInputDocument();
      childDocument.addField(ID_FIELD, childDocIDS[i]);
      childDocument.addField("cat", "childDocument");
      childDocument.addField("title", titleVals[i]);

      SolrInputDocument grandChildDocument = new SolrInputDocument();
      grandChildDocument.addField(ID_FIELD, grandChildIDS[i]);

      childDocument.addChildDocument(grandChildDocument);
      parentDocument.addChildDocument(childDocument);

      try {
        Long version = addAndGetVersion(parentDocument, null);
        assertNotNull(version);
      } catch (Exception e) {
        fail("Failed to add document to the index");
      }
      if (random().nextBoolean()) {
        assertU(commit());
      }
    }

    assertU(commit());
    assertQ(
        req("q", "*:*"),
        "//*[@numFound='" + (parentIDS.length + childDocIDS.length + grandChildIDS.length) + "']");
  }

  private void testParentFilterJSON() throws Exception {

    String[] tests =
        new String[] {
          "/response/docs/[0]/id=='1'",
          "/response/docs/[0]/_childDocuments_/[0]/id=='2'",
          "/response/docs/[0]/_childDocuments_/[0]/cat=='childDocument'",
          "/response/docs/[0]/_childDocuments_/[0]/title=='" + titleVals[0] + "'",
          "/response/docs/[1]/id=='4'",
          "/response/docs/[1]/_childDocuments_/[0]/id=='5'",
          "/response/docs/[1]/_childDocuments_/[0]/cat=='childDocument'",
          "/response/docs/[1]/_childDocuments_/[0]/title=='" + titleVals[1] + "'"
        };

    assertJQ(
        req(
            "q", "*:*",
            "sort", "id asc",
            "fq", "subject:\"parentDocument\" ",
            "fl",
                "*,[child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"),
        tests);

    assertJQ(
        req(
            "q", "*:*",
            "sort", "id asc",
            "fq", "subject:\"parentDocument\" ",
            "fl",
                "id, cat, title, [child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"),
        tests);

    // shows if parentFilter matches all docs, then there are effectively no child docs
    assertJQ(
        req(
            "q",
            "*:*",
            "sort",
            "id asc",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id,[child childFilter='cat:childDocument' parentFilter=\"*:*\"]"),
        "/response=="
            + "{'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'1'},{'id':'4'}]}");
  }

  private void testSubQueryParentFilterJSON() throws Exception {

    String[] tests =
        new String[] {
          "/response/docs/[0]/id=='1'",
          "/response/docs/[0]/children/docs/[0]/id=='2'",
          "/response/docs/[0]/children/docs/[0]/cat=='childDocument'",
          "/response/docs/[0]/children/docs/[0]/title=='" + titleVals[0] + "'",
          "/response/docs/[1]/id=='4'",
          "/response/docs/[1]/children/docs/[0]/id=='5'",
          "/response/docs/[1]/children/docs/[0]/cat=='childDocument'",
          "/response/docs/[1]/children/docs/[0]/title=='" + titleVals[1] + "'"
        };

    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "sort",
            "id asc",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.fq",
            "cat:childDocument",
            "children.sort",
            "_docid_ asc"),
        tests);
    assertJQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id,children:[subquery]",
            "sort",
            "id asc",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.fq",
            "cat:childDocument",
            "children.sort",
            "_docid_ asc"),
        tests);
  }

  private void testParentFilterXML() {

    String tests[] =
        new String[] {
          "//*[@numFound='2']",
          "/response/result/doc[1]/str[@name='id']='1'",
          "/response/result/doc[1]/doc[1]/str[@name='id']='2'",
          "/response/result/doc[1]/doc[1]/str[@name='cat']='childDocument'",
          "/response/result/doc[1]/doc[1]/str[@name='title']='" + titleVals[0] + "'",
          "/response/result/doc[2]/str[@name='id']='4'",
          "/response/result/doc[2]/doc[1]/str[@name='id']='5'",
          "/response/result/doc[2]/doc[1]/str[@name='cat']='childDocument'",
          "/response/result/doc[2]/doc[1]/str[@name='title']='" + titleVals[1] + "'"
        };

    assertQ(
        req(
            "q", "*:*",
            "sort", "id asc",
            "fq", "subject:\"parentDocument\" ",
            "fl", "*,[child childFilter='cat:childDocument']"),
        tests);

    assertQ(
        req(
            "q", "*:*",
            "sort", "id asc",
            "fq", "subject:\"parentDocument\" ",
            "fl", "id, cat, title, [child childFilter='cat:childDocument']"),
        tests);
  }

  private void testSubQueryParentFilterXML() {

    String tests[] =
        new String[] {
          "//*[@numFound='2']",
          "/response/result/doc[1]/str[@name='id']='1'",
          "/response/result/doc[1]/result[@name='children'][@numFound=1]/doc[1]/str[@name='id']='2'",
          "/response/result/doc[1]/result[@name='children'][@numFound=1]/doc[1]/str[@name='cat']='childDocument'",
          "/response/result/doc[1]/result[@name='children'][@numFound=1]/doc[1]/str[@name='title']='"
              + titleVals[0]
              + "'",
          "/response/result/doc[2]/str[@name='id']='4'",
          "/response/result/doc[2]/result[@name='children'][@numFound=1]/doc[1]/str[@name='id']='5'",
          "/response/result/doc[2]/result[@name='children'][@numFound=1]/doc[1]/str[@name='cat']='childDocument'",
          "/response/result/doc[2]/result[@name='children'][@numFound=1]/doc[1]/str[@name='title']='"
              + titleVals[1]
              + "'"
        };

    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "*,children:[subquery]",
            "sort",
            "id asc",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.fq",
            "cat:childDocument",
            "children.sort",
            "_docid_ asc"),
        tests);

    assertQ(
        req(
            "q",
            "*:*",
            "fq",
            "subject:\"parentDocument\" ",
            "fl",
            "id,children:[subquery]",
            "sort",
            "id asc",
            "children.q",
            "{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
            "children.fq",
            "cat:childDocument",
            "children.sort",
            "_docid_ asc"),
        tests);
  }
}
